/*
 * TCSS 558
 * Distributed HashMap
 * Fall 2013
 * James Marquardt
 * Matt Adams
 * Justin Wong
 */
package model;
import static com.google.common.base.Preconditions.*;
import java.io.Serializable;
import java.util.Collections;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;

import node.NodeActual;

import tools.CheckArgument;
import tools.Log;

/**
 * Contains entry data for actual nodes and their virtual nodes.
 */
public class Entries implements Serializable {
    
    /**
     * Serializable ID.
     */
    private static final long serialVersionUID = -5640717934067734440L;
    /**
     * Local hash table for entries. Synchronized so methods do not have to
     * be synchronized.
     */
    private Map<Integer, Set<Entry>> entries = null;
    
    private final int mValueOfChord;
    /**
     * Creates an empty repository for entries.
     */
    public Entries(final int mValueOfChord) { 
        this.entries = Collections.synchronizedMap(new TreeMap<Integer, Set<Entry>>());
        this.mValueOfChord = mValueOfChord;
    }
    
    /**
     * Stores a set of entries to the local hash table.
     * 
     * @param entriesToAdd Set of entries to add to the repository.
     */
    public final void addAll(Set<Entry> entriesToAdd) {
        checkNotNull(entriesToAdd, "Set of entries to add cannot be null.");
        for (Entry entry : entriesToAdd) {
            this.add(entry, NodeActual.getNodeIDFromKey(entry.getID(), this.mValueOfChord));
        }
    }
    
    /**
     * Stores one entry to the local hash table.
     * 
     * @param entryToAdd Entry to add to the repository.
     */
    public final void add(Entry entryToAdd) {
        add(entryToAdd, entryToAdd.getID());
    }
    
    /**
     * Stores one entry to the local hashtable at a speciefied key.
     * 
     * @param entryToAdd The entry to add to the table.
     * @param key The key the entry will be stored at.
     */
    public final void add(Entry entryToAdd, int key) {
    	checkNotNull(entryToAdd, "Entry being added cannot be null.");
        Set<Entry> entrySet = null;
        synchronized (this.entries) {            
            if (this.entries.containsKey(key)) {
                entrySet = this.entries.get(key);
            } else {
                entrySet = new HashSet<Entry>();   
                this.entries.put(key, entrySet);
            }
            if (entrySet.contains(entryToAdd)) {
                entrySet.remove(entryToAdd);
            }
            entrySet.add(entryToAdd);            
        }
    }
    
    /**
     * Removes the given entry from the local hash table.
     * 
     * @param entryToRemove Entry to remove from the hash table.
     */
    public final void remove(final Entry entryToRemove) {
        remove(entryToRemove, entryToRemove.getID());
    }
    
    /**
     * Removes the given entry from the local hash table.
     * 
     * @param entryToRemove Entry to remove from the hash table.
     */
    public final void remove(final Entry entryToRemove, final int key) {
        checkNotNull(entryToRemove, "Entry being removed cannot be null.");
        Set<Entry> entrySet = null;
        synchronized (this.entries) {
            if (this.entries.containsKey(key)) {
                entrySet = this.entries.get(key);
                entrySet.remove(entryToRemove);
                Log.out("REMOVED ENTRY: " + entryToRemove.toString() + " Entry Set Size: " + entrySet.size());
                
                if (entrySet.isEmpty()) {
                    this.entries.remove(key);
                }
            }
        }
    }
    
    /**
     * Removes all entries in the given set.
     * 
     * @param entriesToRemove Set of entries to remove.
     */
    public final void removeAll(Set<Entry> entriesToRemove) {
        checkNotNull(entriesToRemove, "Entries to remove cannot be null.");
        for (Entry entry : entriesToRemove) {
            this.remove(entry, NodeActual.getNodeIDFromKey(entry.getID(), this.mValueOfChord));
        }
    }
    
    /**
     * Returns an unmodifiable map of all stored entries.
     * 
     * @return Unmodifiable map of all stored entries.
     */
    public final Map<Integer, Set<Entry>> getEntries() {
        return Collections.unmodifiableMap(this.entries);
    }
    
    public final Set<Entry> getEntries(final int id) {
        synchronized (this.entries) {
            if (this.entries.containsKey(id)) {
                Set<Entry> entriesWithID = this.entries.get(id);
                return new HashSet<Entry>(entriesWithID);
            }
        }
        return new HashSet<Entry>();
    }
    
    public Set<Entry> getEntriesAsSet() {
        Set<Entry> result = new HashSet<Entry>();
        synchronized (this.entries) {
            for (int id : entries.keySet()) {
                Set<Entry> temp = entries.get(id);
                for (Entry entry : temp) {
                    result.add(entry);
                }
            }
        }
        return result;
    }
    
    /**
     * Returns all Entries contains in the interval (startID, endID), where
     * startID is exclusive and endID is inclusive.
     * 
     * @param startID Start interval ID
     * @param endID End interval ID
     * @return
     */
    public final Set<Entry> getEntriesInInterval(final int startID, final int endID) {
        Set<Entry> result = new HashSet<Entry>();
        synchronized (this.entries) {
            for (final int nextID : this.entries.keySet()) {
                if (CheckArgument.isInInterval(nextID, startID, endID)) {
                    Set<Entry> entriesForID = this.entries.get(nextID);
                    for (Entry entryToAdd : entriesForID) {
                        result.add(entryToAdd);
                    }
                }
            }
        }
        return result;
    }   
    
    /**
     * Returns a formatted string of all entries stored in the local hash table.
     * 
     * @return String representation of all stored entries.
     */
    public final String toString() {
        StringBuilder result = new StringBuilder("Entries:\n");
        synchronized (this.entries) {
            for (Map.Entry<Integer, Set<Entry>> entry : this.entries.entrySet()) {
                Iterator<Entry> itr = entry.getValue().iterator();
                while (itr.hasNext()) {
                    Entry next = itr.next();
                    result.append("  key = " + next.getID() + ", value = " +  (String) next.getValue() + "\n");
                } 
                result.append("  All Entries shown for node ID = " + entry.getKey() + "\n");
            }
        }
        return result.toString();
        
    }
}
